Letztes Update: 27.05.2016
Behandelte Befehle: CheckBox, RadioButton, ComboBox, Slider, FileChooser, ImageView, FlowPane, GridPane

Aus dem letzten Kapitel wissen wir schon ziemlich viel darüber, wie man eine GUI programmiert. In diesem Kapitel lernen wir Details zur internen Organisation (Szenengraph) kennen, weitere Kontrollen und Layouts und noch etwas über CSS-Klassen.

23.1 Szenengraph und Controls

JavaFX verwendet einen Szenengraph, um die Bestandteile einer GUI zu verwalten. Der Szenengraph wird in der Klasse Scene gespeichert. Der Graph folgt einer Baumstruktur, der aus Knoten (engl. node) besteht. Ein Knoten kann "Kinder" haben (im Bild unterhalb des Knotens gezeigt und mit Linien mit dem Elternknoten verbunden). Einen Knoten mit Kindern nennt man branching node (branch = Zweig), einen Knoten ohne Kinder nennt man leaf node (leaf = Blatt). Den "obersten" Knoten nennt man root node (root = Wurzel).

Was bedeutet das für unsere GUIs? Die Blätter (leaf nodes) sind die sichtbaren Komponenten (Buttons, Textfelder, Slider etc.), die Zweigknoten (branching nodes) sind die unsichtbaren, strukturellen Elemente (BorderPane, HBox, VBox etc.):

Man kann sich das auch wie eine Verzeichnisstruktur vorstellen. Die Blätter sind die Dateien, also etwas Konkretes und die Verzeichnisse sind die Zweigknoten, die zur Strukturierung da sind.

In der theoretischen Informatik nennt man Blattknoten auch Terminale und Zweigknoten Nicht-Terminale.

In der Computergrafik, insbesondere auch in Game Engines, werden Szenengraphen oft eingesetzt, um die Transformationen zu verwalten: die Zweigknoten enthalten dort Transformationen (Translation, Rotation, Skalierung), wohingegen die Blattknoten konkrete Formen (Rechteck, Ellipse, Quader, Kugel ...) darstellen. Im Gegensatz zu Processing, wo jede Transformation "von Hand" direkt im Code vorgenommen wird, nennt man die Vorgehensweise eines Szenengraphen auch retained mode. Das bedeutet, dass intern ein "Modell" der Zeichnung vorgehalten (= retained) wird, welches erst zum Zeitpunkt des Renderns in konkrete Anweisungen umgewandelt wird.

Nachdem wir wissen, wie ein Szenengraph strukturell aufgebaut ist, beschäftigen wir uns zunächst mit einigen konkreten "Blatt-Typen" oder "Controls".

CheckBox

Eine CheckBox (engl. to check = abhaken) erlaubt es, eine oder mehrere Optionen auszuwählen:

Im Gegensatz zu einem Radiobutten kann beliebig viele oder keine der Optionen markieren.

Im Code platzieren Sie einfach irgendwo Ihre Boxen:

public class CheckboxDemo extends Application {

@Override
public void start(Stage stage) {
CheckBox check1 = new CheckBox("Milch");
CheckBox check2 = new CheckBox("Brot");
CheckBox check3 = new CheckBox("Butter");
Button kaufen = new Button("Kaufen");

VBox pane = new VBox(check1, check2, check3, kaufen);
pane.setPadding(new Insets(15));
pane.setSpacing(25);

Scene scene = new Scene(pane);
stage.setTitle("Schöner Einkaufen");
stage.setScene(scene);
stage.show();
}
}

Anschließend können Sie den Zustand einer Box mit der Methode isSelected() abfragen, die true (ausgewählt) oder false (nicht ausgewählt) zurückgibt. Hier ein Beispiel, wo wir eine Aktion an den Button hängen und für jede Auswahl das entsprechende "Produkt" ausdrucken:

kaufen.setOnAction(e -> {
System.out.println("Sie kaufen:");
if (check1.isSelected()) {
System.out.println("Milch");
}
if (check2.isSelected()) {
System.out.println("Brot");
}
if (check3.isSelected()) {
System.out.println("Butter");
}
});

In CSS wird das Innere der Box wie folgt gestylt:

.check-box .box {
-fx-background-color: white;
}

Die Box besitzt nochmal eine eigene CSS-Klasse. Mit obiger Syntax wählt man also Elemente der Klasse .box innerhalb eines Elements der Klasse .check-box aus..

RadioButton

Ein radio button ist wie eine Checkbox mit dem Unterschied, dass eine Reihe von Radiobuttons zusammenhängen, so dass immer nur einer zur Zeit angewählt sein kann:

Im Code muss man die RadioButton Objekte in eine Gruppe hineinstecken. Diese Gruppe ist ein Objekt vom Typ ToggleGroup:

public class RadioButtonDemo extends Application {

public void start(Stage stage) {
Label label = new Label("Getränk wählen:");

ToggleGroup getraenke = new ToggleGroup();

RadioButton radio1 = new RadioButton("Cappucino");
radio1.setToggleGroup(getraenke);
radio1.setSelected(true);

RadioButton radio2 = new RadioButton("Latte");
radio2.setToggleGroup(getraenke);
RadioButton radio3 = new RadioButton("Espresso");
radio3.setToggleGroup(getraenke);

Button kaufen = new Button("Kaufen");

VBox pane =
new VBox(label, radio1, radio2, radio3, kaufen);
pane.setPadding(new Insets(15));
pane.setSpacing(25);

Scene scene = new Scene(pane);
stage.setTitle("Automat");
stage.setScene(scene);
stage.show();
}
}

Ähnlich wie bei den Checkboxen, können Sie die Methode isSelected() verwenden, um den Zustand abzufragen:

kaufen.setOnAction(e -> {
System.out.println("Sie kaufen:");
if (radio1.isSelected()) {
System.out.println("Cappucino");
}
if (radio2.isSelected()) {
System.out.println("Latte");
}
if (radio3.isSelected()) {
System.out.println("Espresso");
}
});

Alternativ können Sie die ToggleGroup fragen, welches Objekt denn ausgewählt ist. Da ToggleGroup verschiedene Typen erlaubt, müssen Sie zuvor casten:

kaufen.setOnAction(e -> {
String produkt =
((RadioButton)getraenke.getSelectedToggle()).getText();
System.out.println("Sie kaufen" + produkt);
});

In CSS wird das Innere des Radiozirkels wie folgt gestylt:

.radio-button .radio {
-fx-background-color: white;
}

ComboBox

Ein Dropdown-Menü heißt in JavaFX ComboBox:

Das ComboBox-Objekt verwaltet intern eine Liste für die Einträge. Diese Einträge können beliebige Objekte sein, der Einfachheit halber verwenden wir hier Strings. Man setzt die Einträge, indem man zunächst mit getItems() die Liste aus der ComboBox holt und dann addAll() auf dieser Liste anwendet. Mit setValue() gibt man an, welcher Eintrag zu Beginn angezeigt werden soll:

public class ComboBoxDemo extends Application {
public void start(Stage stage) {

Label label = new Label("Choose Nationality");
label.setFont(new Font(20));

ComboBox comboBox = new ComboBox();
comboBox.getItems().addAll("German",
								"American", "French");
comboBox.setValue("German");

// Layout
BorderPane pane = new BorderPane(comboBox);
pane.setTop(label);
pane.setBottom(ok);
pane.setPadding(new Insets(30));

Scene scene = new Scene(pane);
stage.setTitle("Where from");
stage.setScene(scene);
stage.show();
}
}
Button ok = new Button("OK");
ok.setOnAction(e -> {
String choice = (String)comboBox.getValue();
System.out.println("Selected: " + choice);
});

Wenn man die Hintergrundfarbe der ComboBox ändert...

.combo-box {
-fx-background-color: moccasin;
}

...betrifft das nur das vordergründig sichtbare Element.

ComboBox mit anderem Hintergrund

Um den Hintergrund des aufklappbaren (popup) Menüs zu stylen, greift man auf die CSS-Klasse .combo-box-popup und deren enthaltenen Klassen zu:

.combo-box-popup .list-view .list-cell {
-fx-background-color: moccasin;
}

Jetzt sind stimmt jedoch das Styling nicht mehr für das jeweils markierte Element. Dazu müssen wir noch auf Pseudoklassen zugreifen (siehe Abschnitt 23.4):

.combo-box-popup .list-view .list-cell {
-fx-background-color: moccasin;
-fx-text-fill: black;
}

.combo-box-popup .list-view .list-cell:filled:hover
{
-fx-background-color: goldenrod;
-fx-text-fill: white;
}

Jetzt haben wir unsere ComboBox vernünftig eingefärbt:

ComboBox vernünftig eingefärbt

Sie sehen hier, dass komplexere (zusammengesetzte) GUI-Elemente ein differenzierteres Styling erfordern.

Übungsaufgaben

(a) Shopping Filter

Programmieren Sie eine Suchmaske bzw. einen Suchfilter für einen Bekleidungs-Shop. Stylen Sie das Fenster mit CSS. (Im Beispiel sind die Farben darkgoldenrod und moccasin verwendet.) Die Optionen bei Warengruppe sind: Socken, Mützen, Handtaschen.

Wenn man auf "Suche" klickt, sollte ein Suchtext erzeugt werden. Zum Beispiel bei

erscheint dieser Text:

Suche Mützen (rot gelb) zwischen 10 und 100€

Tipp: Beim Zusammensetzen des Strings erleichtert der ternäre Operator (Kap. 18.2) das Leben. Es geht aber auch mit If-Anweisungen.

(b) Shopping Backend

Nachdem wir die GUI haben, programmieren wir ein kleines Backend dazu: Einen Shop mit Produkten, die wir mit unserer GUI durchsuchen können.

Dazu benötigen wir lediglich zwei Klassen: Shop und Product.

Die Klasse Product hat vier Instanzvariablen:

  • category
  • color
  • price
  • title
  • (hier kommt ein griffiger Produkttitel rein, z.B. "Falke Wandersocke F11")

Zusätzlich brauchen wir einen Konstruktor mit den vier Parametern und eine aussagekräftige toString()-Methode.

Die Klasse Shop enthält eine Liste von Product-Objekten und kann diese Liste auch mit einer Getter-Methode zurückgeben.

Zum Testen können Sie im Konstruktor von Shop auch einfach die Produktliste befüllen.

Erstellen Sie einfach in Ihrer GUI-Klasse einen Shop (z.B. direkt in der start()-Methode), der mit mindestens 5 Artikeln befüllt sein sollte. Wenn der Benutzer auf "Suche" klickt, sollten die Produkte ausgegeben werden, auf die die Eigenschaften zutreffen (können natürlich mehrere sein).

Die Ausgabe erfolgt auf der Konsole. Die Ausgabe sollte eine Zusammenfassung der Filtereinstellung beinhalten (Ergebnis von a) und dann die Artikel aufzählen. Abschließend können Sie noch anzeigen, wie viele Artikel gefunden wurden. Zum Beispiel:

Suche Socken (rot blau) unter 10€
> Wandersocke F11 (Socken) rot 9.5 EUR
> Tennissocke X13 (Socken) blau 5.5 EUR
> Supersocke 15 (Socken) blau 9.0 EUR
Found 3 products.

23.2 Events

Bislang kennen wir den Fall, dass wir auf einen Button-Click reagieren wollen. Wir hängen dann ein Stück Code (Lambda-Ausdruck) mit Hilfe von setOnAction an den Button. Den Button-Click nennt man auch Event (engl. für Ereignis). Ein Event ist ein punktuelles Ereignis in der Zeit, wo ganz präzise definiert ist, wann dieser Zeitpunkt eintritt. Bei einem Mausklick zum Beispiel muss man genau angeben, ob man auf das Runterdrücken oder auf das Loslassen der Maustaste reagieren möchte.

Wenn man mit Code auf solche Events reagiert, spricht man auch von Event-basierter Programmierung. In diesem Abschnitt lernen wir über die neuen Controls Slider (Schieberegler) und TextArea (Texteingabebereich) auch neue Arten von Events kennen.

Slider und Mausevents

Ein Slider ist ein Schieberegler, mit dem sich ein Wert einstellen lässt, der über eine größeres Spektrum läuft (z.B. von 0 bis 100) oder kontinuierlich ist (z.B. eine gebrochene Zahl zwischen 0 und 1).

Slider kann man z.B. verwenden, um eine Farbe mit Regler für rot, grün und blau einzustellen:

Ein Slider hat einen Konstruktor, dem man das gewünschte Wertspektrum übergibt (MIN und MAX) sowie den anfangs eingestellten Wert (VALUE):

new Slider(MIN, MAX, VALUE);

Im Code sehen wir drei Slider für die Farben Rot, Grün und Blau:

public class SliderDemo extends Application {

@Override
public void start(Stage stage) {

Slider redSlider = new Slider(0, 1, 0);
Slider greenSlider = new Slider(0, 1, 0);
Slider blueSlider = new Slider(0, 1, 0);

VBox sliderPane = new VBox(redSlider, greenSlider,
											 blueSlider);

Rectangle colorField = new Rectangle(200, 100);

// Layout
BorderPane pane = new BorderPane(colorField);
pane.setTop(sliderPane);
pane.setPadding(new Insets(30));

Scene scene = new Scene(pane, 300, 250);

stage.setTitle("Colors");
stage.setScene(scene);
stage.show();
}

}

Mausevents

Beim Slider wollen immer dann reagieren, wenn der Schieberegler verschoben wird und zwar noch während er verschoben wird. Denn es soll sich während des Verschiebens die Hintergrundfarbe anpassen.

Das Event, das wir suchen, ist also, dass der Regler um ein kleines Stück verschoben wurde. Dazu muss die Maus über dem Regler gedrückt worden sein und die Maustaste muss sich noch im niedergedrücketen Zustand befinden. Man nennt dies auch "dragging" (engl. für ziehen/schleifen).

Wir können also eine Funktion an das Event mouse dragged hängen. Das bedeutet, dass der User auf das Element klickt und die Maus im gedrückten Zustand bewegt.

Als Reaktion auf dieses Event stellen wir eine neue Farbe für das Rechteck ein. Der Einfachheit halber hängen wir dreimal die gleiche Funktion an die jeweiligen Slider (eigentlich unschön wegen Code-Duplizierung):

// Aktionen

redSlider.setOnMouseDragged(e -> {
double red = redSlider.getValue();
double green = greenSlider.getValue();
double blue = blueSlider.getValue();
colorField.setFill(new Color(red, green, blue, 1.0));
});

greenSlider.setOnMouseDragged(e -> {
double red = redSlider.getValue();
double green = greenSlider.getValue();
double blue = blueSlider.getValue();
colorField.setFill(new Color(red, green, blue, 1.0));
});

blueSlider.setOnMouseDragged(e -> {
double red = redSlider.getValue();
double green = greenSlider.getValue();
double blue = blueSlider.getValue();
colorField.setFill(new Color(red, green, blue, 1.0));
});

Funktionen an Variablen binden

Um die Code-Duplizierung oben zu vermeiden, müsste man die selbe Funktion an alle Sliderobjekte binden. Wenn wir zunächst den Lambda-Ausdruck einer lokalen Variable zuweisen könnten, könnte das klappen.

Doch welchen Typ hätte diese Variable? Wenn wir uns die Dokumentation von setOnMouseDragged ansehen, finden wir "EventHandler<? super MouseEvent>". Das heißt, unsere Funktion ist vom Typ "EventHandler" und ist ein Generic.

Wir definieren also unsere Variable mit dem Code wie folgt:

EventHandler<MouseEvent> sliderHandler = e -> {
double red = redSlider.getValue();
double green = greenSlider.getValue();
double blue = blueSlider.getValue();
colorField.setFill(new Color(red, green, blue, 1.0));
};

Jetzt ist unser Lambda-Ausdruck an die Variable sliderHandler gebunden und wir können dies drei Mal den entsprechenden Slider-Funktionen übergeben:

redSlider.setOnMouseDragged(sliderHandler);
greenSlider.setOnMouseDragged(sliderHandler);
blueSlider.setOnMouseDragged(sliderHandler);

Unsere Code-Duplizierung ist verschwunden, d.h. wenn wir den Handler-Code verändern, müssen wir dies nicht an drei Stellen tun, sondern nur noch an einer.

TextArea und Tastaturevents

Sie kennen bereits das Element TextField, um einzeilige Texteingaben zu erlauben.

Eine TextArea erlaubt die Eingaben von mehrzeiligen Texten, z.B. für Kommentare oder Kurzbeschreibungen.

Ein Text-Bereich

Code-Beispiel:

public class TextAreaDemo extends Application {

public void start(Stage stage) {

// Controls
Label label = new Label("Kommentar");
label.setFont(new Font(20));

Button ok = new Button("OK");

TextArea textArea = new TextArea();

// Layout
BorderPane pane = new BorderPane(textArea);
pane.setPadding(new Insets(10));
pane.setTop(label);
pane.setBottom(ok);

Scene scene = new Scene(pane, 300, 200);

// Fenster
stage.setTitle("Kommentar eingeben");
stage.setScene(scene);
stage.show();
}
}

Wenn der Platz zur Seite oder nach unten nicht ausreicht, fügt das Element automatisch Scroll-Schaltflächen hinzu.

Text-Bereich mit Scroll-Schaltflächen

Man kann auch einstellen, dass Text wortweise umgebrochen wird:

.text-area {
-fx-wrap-text: false;
}

Dann sieht das so aus:

Text-Bereich mit Textumbruch

Tastaturevents

Sie können die Tatsache, dass ein Benutzer ein Zeichen eingibt auch nutzen, um den aktuellen Text zu analysieren und gegebenenfalls zu manipulieren, z.B. um bestimmte Eingaben zu erzwingen (korrekte e-Mail-Adresse) oder um Rechtschreibkorrekturen durchzuführen.

Es gibt zwei wichtige Events, die man dazu verwenden kann:

textArea.setOnKeyPressed(e -> {
System.out.println("Taste gedrückt.");
});

textArea.setOnKeyReleased(e -> {
System.out.println("Taste losgelassen.");
}

Der folgende Code prüft, ob ein Wort "foo" eingegeben wurde und ersetzt das Wort durch "BUMM":

textArea.setOnKeyReleased(e -> {
System.out.println("Taste losgelassen.");

String text = textArea.getText();
if (text.contains("foo")) {
text = text.replace("foo", "BUMM");
textArea.setText(text);
textArea.positionCaret(text.length());
}
});

Beachten Sie, dass wir hier mit Absicht auf das Event "loslassen" reagieren. Warum? Testen Sie es mal mit "gedrückt" und vergleichen Sie, was passiert.

ListView und SelectionModel

Eine weitere häufige Komponente ist eine Listendarstellung, wo eine oder mehrere Option/en angewählt werden können. In unserem MyTunes-Beispiel könnte das wie folgt aussehen:

window with list view

Das links markierte Element soll im rechten Teil des Fensters beschrieben werden.

Ähnlich wie bei der ComboBox werden die Optionen von Java als Liste verwaltet. Im einfachsten Fall kann man eine Liste von Strings verwenden. Hier ein Beispiel:

Einfaches Beispiel einer ListView

Man holt sich die Liste mit getItems() und befüllt sie mit Strings:

public class ListDemo extends Application {

@Override
public void start(Stage stage) {
ListView listView = new ListView();
listView.getItems().add("One");
listView.getItems().add("Two");
listView.getItems().add("Three");
listView.getItems().add("Four");
listView.getItems().add("Five");

StackPane root = new StackPane();
root.getChildren().add(listView);

Scene scene = new Scene(root, 180, 110);

stage.setTitle("List Demo");
stage.setScene(scene);
stage.show();
}

public static void main(String[] args) {
launch(args);
}

}
Man kann aber auch eine Liste von beliebigen Objekten nehmen. Dann wird für die GUI jeweils die toString() Methode der Objekte verwendet, um das Objekt als Option darzustellen.

Im folgenden Code ist dies zu sehen. Die Klasse MyTunes stellt eine Liste vom Medium-Objekten bereit, die direkt dem ListView übergeben wird.

public class MyTunesUI extends Application {

private MyTunes myTunes = new MyTunes();

@Override
public void start(Stage stage) {

myTunes.initSampleData();

// title
Label titleL = new Label("MyTunes");
titleL.setId("title");
HBox titleP = new HBox(titleL);
titleP.setId("titlepane");

// buttons
Button closeB = new Button("Close");
Button newB = new Button("New");
HBox buttonP = new HBox(newB, closeB);
buttonP.getStyleClass().add("buttonpane");

// liste
ListView mediaL = new ListView();
mediaL.getItems().addAll(myTunes.getMedia());

// description
Label typeL = new Label();
Label songL = new Label();
Label personL = new Label();

VBox descP = new VBox(typeL, songL, personL);
descP.setPadding(new Insets(0, 10, 10, 20));
descP.setSpacing(10);

// layout
BorderPane root = new BorderPane();
root.setTop(titleP);
root.setBottom(buttonP);
root.setLeft(mediaL);
root.setCenter(descP);
root.setId("root");

// actions
mediaL.getSelectionModel().selectedItemProperty().addListener((obsValue, oldValue, newValue) -> {
Medium m = (Medium) mediaL.getSelectionModel().getSelectedItem();
typeL.setText(m instanceof Song ? "Song" : "Movie");
songL.setText("Title: " + m.getTitle());
personL.setText(m instanceof Song ? "Artist: " + ((Song) m).getArtist()
				: "Director: " + ((Movie) m).getDirector());
});

closeB.setOnAction(e -> {
Platform.exit();
});

newB.setOnAction(e -> {
NewMediumDialog dialog = new NewMediumDialog();
dialog.show();
});

mediaL.getSelectionModel().select(0);
Scene scene = new Scene(root, 600, 400);
scene.getStylesheets().add("style.css");

stage.setTitle("MyTunes!");
stage.setScene(scene);
stage.show();
}

public static void main(String[] args) {
launch(args);
}

}

Eine weitere Besonderheit an diesem Code: Es wird hier nicht auf das "setOnAction"-Event gelauscht (schließlich kann sich eine Option in einer Liste auch durch Betätitung einer Cursortaste ändern). Stattdessen hängen wir uns an eine "Property". Das ist ein besonderer Mechanismus in Java, der erlaubt, die Veränderung von Werten zu verfolgen. In diesem Fall verfolgen wir die Änderung der Eigenschaft "selected item", d.h. jedesmal wenn die markierte Option sich ändert, wird unser Code im Lambda-Ausdruck ausgeführt.

Das dazugehörige CSS sieht wie folgt aus:

#title {
-fx-font-size: 34px;
}

#titlepane {
-fx-alignment: CENTER;
-fx-padding: 5px 10px 20px 10px;
}

.radio-button {
-fx-font-size: 16px;
-fx-padding: 10px;
}

#root {
-fx-padding: 20px;
}

.label {
-fx-font-size: 20px;
}

.button {
-fx-font-size: 20px;
}

.buttonpane {
-fx-padding: 5px;
-fx-spacing: 10px;
-fx-alignment: CENTER_RIGHT;
}

Dialogfenster: FileChooser

Bislang haben Sie immer mit einem Fenster gearbeitet, aber normalerweise können mehrere Fenster aufgehen. Wenn ein Fenster aufgeht, um eine relativ kleine Menge Informationen abzufragen, nennt man das ein Dialogfenster.

Eine spezielle Art Dialogfenster ist ein modales Dialogfenster, das heißt, das neue Fenster blockiert alle anderen Fenster, solange bis die Eingabe in neuen Fenster abgeschlossen ist oder abgebrochen wird.

Eine typische Situation für einen solchen Dialog ist die Eingabe eines Dateipfads. Das Dialogfenster wird über einen Button (hier: Datei wählen) gestartet. Alternativ kann man den Pfad in einem Textfeld eingeben.

Hier ist das Verhalten beim Ändern der Fenstergröße wichtig. Es sollte immer das Textfeld vergrößert bzw. verkleinert werden:

Wenn man auf den Button drückt, kommt das Dialogfenster hoch. Dieses Fenster wird von JavaFX mit der Klasse FileChooser zur Verfügung gestellt. Beachten Sie die Möglichkeit, nach Dateitypen zu filtern (durch Auswahl in der ComboBox unten).

Nach Auswahl der Datei soll der komplette Pfad im Textfeld erscheinen:

Im Code sieht das wie folgt aus. Beachten Sie die Verwendung von BorderPane, damit das Textfeld sich vergrößert. Der maßgebliche Code für den FileChooser steht in der Aktion, die an den Button geheftet ist.

public class FileChooserDemo extends Application {

@Override
public void start(Stage stage) {

Label label = new Label("Ziel:");
TextField textfield = new TextField();
Button dateiWaehlen = new Button("Datei wählen..");

// BorderPane, damit Textfeld vergrößert wird
BorderPane pane = new BorderPane(textfield);
pane.setLeft(label);
pane.setRight(dateiWaehlen);
pane.setPadding(new Insets(20));

// Statische Methoden für Alignierung
BorderPane.setAlignment(label, Pos.CENTER);
BorderPane.setAlignment(dateiWaehlen, Pos.CENTER);

// Aktion
dateiWaehlen.setOnAction(e -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Datei wählen");
fileChooser.getExtensionFilters().addAll(
				new ExtensionFilter("Textdateien",
						"*.txt"),
				new ExtensionFilter("Bilddateien",
						"*.png", "*.jpg", "*.gif"),
				new ExtensionFilter("Alle Dateien",
						"*.*"));
File selectedFile =
		 fileChooser.showOpenDialog(stage);
if (selectedFile != null) {
		textfield.setText(selectedFile.toString());
}
});

Scene scene = new Scene(pane, 400, 120);
stage.setTitle("Dateiwahl");
stage.setScene(scene);
stage.show();
}
}

23.3 Layout

Oracle layout tutorial

Im letzten Kapitel haben Sie gelernt, dass das Layout über Layout-Klassen wie BorderPane gehandhabt wird:

In diesem Kapitel haben Sie das Konzept des Szenengraphen kennengelernt. Die Layout-Klassen können Sie sich als Zweigknoten vorstellen, die konkrete Elemente wie Buttons enthalten oder wiederum Layout-Klassen. Im letzten Kapitel haben Sie bereits ein Beispiel eines "verschachtelten" Layouts gesehen:

Im folgenden erweitern wir unser Repertoire an Layout-Klassen um FlowPane und GridPane.

FlowPane

Eine FlowPane ordnet die Komponenten als flexibles Gitter an. Die Elemente werden von links nach rechts angeordnet. Wenn kein Platz mehr da ist, wird in der nächsten "Zeile" fortgefahren. Die Elemente werden also wie Wörter bei einer Textverarbeitung angeordnet und "umgebrochen".

Hier ein Beispiel mit vier Komponenten:

Diese werden umgeordnet, wenn man die Größe des Fensters verändert:

Im folgenden Code werden vier Bilder geladen, die sich im src-Verzeichnis befinden müssen und "pic-1.jpg" und "pic-2.jpg" etc. heißen müssen. Ein Bild wird mit Hilfe der Klasse ImageView geladen und dargestellt. Die Klasse FlowPane funktioniert so, dass man mit getChildren die Liste von Komponenten bekommt und dann mit add dort die neuen Komponenten hinzufügt.

public class LayoutDemos extends Application {

public void start(Stage stage) {

FlowPane flow = new FlowPane();
flow.setPadding(new Insets(20));
flow.setVgap(10);
flow.setHgap(10);
flow.setStyle("-fx-background-color: gray;");

ImageView pages[] = new ImageView[8];
for (int i = 0; i < 4; i++) {
pages[i] = new ImageView(
				new Image("pic-" + (i+1) + ".jpg"));
flow.getChildren().add(pages[i]);
}

Scene scene = new Scene(flow, 450, 450);
stage.setTitle("Flow");
stage.setScene(scene);
stage.show();
}
}

Man kann den "Flow" auch von oben nach unten gehen lassen. Dann verwendet man den Konstruktor:

new FlowPane(Orientation.VERTICAL);

GridPane

Ein GridPane erlaubt eine Ausrichtung an einem Gitter. Man kann die Abstände und Zwischenräume bestimmen und dann Komponenten in die entsprechenden Zellen packen mit (grid enthalte ein Objekt des Typs GridPane):

grid.add(KOMPONENTE, SPALTE, ZEILE);

Wobei SPALTE und ZEILE mit Null beginnen.

Will man, dass ein Objekt mehrere Spalten oder Zeilen umspannt, verwendet man:

grid.add(KOMPONENTE, SPALTE, ZEILE, SPALTENZAHL, ZEILENZAHL);

Das heißt, wenn SPALTENZAHL und ZEILENZAHL gleich 1 sind, entspricht dies dem obigen Kommando. Andernfalls werden mehrere Zellen für die Komponente aufgewendet.

Ein GridPane entwirft man am besten auf dem Papier:

Im Code sieht das dann wie folgt aus. Für die zwei Buttons rechts unten benötigen wir eine HBox. Die Alignierung der beiden Labels werden mit der statischen Methode setHalignment von GridPane angegeben. Wenn Sie setGridLinesVisible auf true setzen, sehen Sie die Gitterlinien, was beim Erstellen sehr nützlich sein kann.

public class GridDemo extends Application {

@Override
public void start(Stage stage) {
GridPane grid = new GridPane();
//grid.setGridLinesVisible(true);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(10, 20, 10, 20));

Label label = new Label("Geben Sie Ihre Infos ein");
label.setFont(new Font(18));
GridPane.setHalignment(label, HPos.CENTER);
grid.add(label, 0, 0, 2, 1);

Label l1 = new Label("Name");
Label l2 = new Label("Matrikelnr.");

GridPane.setHalignment(l1, HPos.RIGHT);
GridPane.setHalignment(l2, HPos.RIGHT);

grid.add(l1, 0, 1);
grid.add(l2, 0, 2);

TextField tf1 = new TextField();
TextField tf2 = new TextField();

grid.add(tf1, 1, 1);
grid.add(tf2, 1, 2);

Button help = new Button("?");
Button ok = new Button("OK");
Button close = new Button("Close");

grid.add(help, 0, 3);

HBox buttons = new HBox(ok, close);
buttons.setAlignment(Pos.CENTER_RIGHT);
buttons.setSpacing(10);

grid.add(buttons, 1, 3);

Scene scene = new Scene(grid);
stage.setTitle("GridPane");
stage.setScene(scene);
stage.show();
}
}

Die fertige Implementation sieht so aus:

Übungsaufgabe

(a) Adressbuch Reloaded

Ändern Sie Ihr Adressbuch-Layout so, dass die Texteingabe-Felder aligniert sind:

Hinweise: Verwenden Sie eine GridPane. Sie können das gesamte Panel zentrieren mit setAlignment(Pos.CENTER).

23.4 CSS, Teil 2

Wir lernen noch ein paar weitere Eigenschaften von CSS.

Styling im Code

Wenn Sie auf die Schnelle einen einzelnen Button oder ähnliches stylen wollen, können Sie CSS-Code auch direkt im Code definieren. Dies sollte man nur in Ausnahmefällen tun, da dies eigentlich der Philosophie von CSS widerspricht:

Button button = new Button("OK");
button.setStyle("-fx-background-color: yellow;");

Eigene Klassen

Sie wissen, dass Sie für einzelne Elemente einen ID erzeugen können:

Button b = new Button("OK");
b.setId("foo");

Im CCS-File können Sie sich dann mit dem Hash-Symbol darauf beziehen:

#foo {
-fx-font-size: 20px;
}

Sie können auch eigene Klassen erzeugen. Der Unterschied zu IDs: eine Klasse kann von mehreren Elementen verwendet werden, ein ID darf nur einmal verwendet werden.

Nehmen wir an, Sie wollen zwei Buttons stylen, dann weisen Sie diesen Ihre neue Klasse zu:

Button b1 = new Button("OK");
b1.getStyleClass().add("footastic");

Button b2 = new Button("Close");
b2.getStyleClass().add("footastic");

Im CSS-File verwenden Sie den Punkt, um auf Klassen zu verweisen:

.footastic {
-fx-font-size: 20px;
}

Eine GUI-Komponente darf mehreren Klassen angehören. Zum Beispiel gehört jeder Button der CSS-Klasse .button an. Im obigen Beispiel fügen wir noch die CSS-Klasse .footastic hinzu. Wir könnten noch weitere hinzufügen.

Eine GUI-Komponente darf natürlich auch sowohl einen ID haben als auch einer oder mehrern Klassen angehören.

Pseudoklassen

Um verschiedene Zustände z.B. eines Button unterschiedlich stylen zu können, gibt es Pseudoklassen. Bei einem Button kann man mit der Pseudoklasse .button:hover den Zustand, dass die Maus über dem Button schwebt stylen:

.button:hover {
-fx-background-color: yellow;
}

In der CSS-Referenz finden Sie weitere Beispiele für Pseudoklassen.

HBox und VBox

Es gibt keine Klassen für HBox und VBox. Wenn Sie diese stylen wollen, müssen Sie selbst eine Klasse hinzufügen, z.B. wie hier:

HBox hbox = new HBox();
hbox.getStyleClass().add("hbox");

Anschließend können Sie die Boxen stylen, die dieser Klasse angehören:

.hbox {
-fx-background-color: white;
-fx-padding: 15;
-fx-spacing: 10;
}

Hintergrundbilder

Sie können Ihren Layout-Objekten ein Hintergrundbild hinzufügen, das sieht dann in CSS so aus, wenn es für das gesamte Fenster (root) gelten soll:

.root {
-fx-background-image: url("bg.jpg");
-fx-background-size: 270, 160;
-fx-background-repeat: no-repeat;
}

Sie können auch Layout-Objekte wie HBox und VBox mit einem Hintergrundbild versehen (mit entsprechendem Selektor).

Das Bild (hier "bg.jpg") muss sich im Verzeichnis src befinden, es wird im obigen Beispiel auf die Größe 270x160 skaliert und nicht wiederholt (d.h. der Hintergrund ist nicht gekachelt). Wenn eine Kachelung erwünscht ist, müssen Sie lediglich die letzte Zeile weglassen.

Übersicht: CSS-Befehle

Hier sind häufige CSS-Befehle tabellarisch aufgeführt, ohne Anspruch auf Vollständigkeit. Eine vollständige Dokumentation finden Sie in in der CSS-Referenz von Oracle.

Text und Schrift

CSS-BefehlBeschreibungBeispielwert/e
-fx-background-color Hintergrundfarbe black, white, yellow, ...
-fx-text-fill Textfarbe s.o.
-fx-font-size Schriftgröße 12px
-fx-font-family Schriftart "Helvetica" (Anführungszeichen beachten)
-fx-font-weight Schrifthervorhebung bold, normal

Kästen

CSS-BefehlBeschreibungBeispielwert/e
-fx-padding Abstand zwischen Rand und Inhaltselementen 15px
-fx-spacing Abstand zwischen den Inhaltselementen 10px
-fx-alignment Ausrichtung der Inhaltselemente baseline-center, baseline-left, center-right, ...
-fx-border-style Randlinie solid, dashed, none
-fx-border-width Dicke der Randlinie 3px
-fx-border-color Farbe der Randlinie black, red, ...
-fx-border-radius Abgerundete Ecken 8px

Effekte

CSS-BefehlBeschreibungBeispielwert/e
-fx-opacity Solidität/Transparenz Wert zwischen 0 (komplett transparent) und 1 (komplett solide)

Übungsaufgabe

(a) Karteikarten-Lernsystem

Entwerfen Sie eine GUI für Ihr Karteikarten-Lernsystem aus Kapitel 23.

Sie benötigen Interface-Elemente für folgende Funktionen:

  • Laden und Speichern eines Karteikastens
  • Anzeigen der Frage und Antwort
  • Buttons etc. zum Steuern einer Lernsitzung

Gut wäre es, den aktuellen Füllzustand der Karteikasten-Fächer zu sehen.

Skizzieren Sie die GUI am besten auf Papier und legen Sie anschließend die Layout-Klassen fest (BoderPane, HBox, VBox, GridPane etc.).

Bei erfolgreicher Programmierung können Sie Ihr Programm nutzen, um damit zu lernen (zum Beispiel für die Java 2 Klausur)...

23.5 Links zum Thema

Leider allesamt englischsprachig: